分类
联系方式
  1. 新浪微博
  2. E-mail

Flutter js 之 JavascriptCoreRuntime

JavascriptCoreRuntime 是 flutter_js 对 JavaScriptCore 引擎的封装,在 macOS 和 iOS 下必须用该引擎。在 Android 下默认为 QuickJS,允许手动切换为 JavaScriptCore。

本文主要研究该类中一些核心功能的实现。

sendMessage 消息通信机制

flutter_js 中提供了一个 Dart-JavaScript 的通信机制,在 JavaScript 侧通过调用 sendMessage 方法可以实现 JavaScript→Dart 通信,我将这种通信方式命名为 sendMessage 消息通信机制。本节介绍在 JavascriptCoreRuntime 中是如何实现这一通信机制的,以及该机制存在的潜在问题。

Dart 回调函数

首先分析向 JavaScript 引擎注册的 Dart 回调:

在 JavascriptCoreRuntime 类中存在一个类方法 _sendMessage,这是一个注册进 JavaScript 引擎的 Dart 方法,其内容是解析 JavaScript 传入 Dart 的数据,并根据约定好的 Channel 名称进行分发。

_sendMessage 是一个类实例方法,在 JavascriptCoreRuntime 的构造方法中,被赋给类中的静态成员 _sendMessageDartFunc。

在 JavascriptCoreRuntime 类中,还有一个静态方法 sendMessageBridgeFunction,该方法中对 _sendMessageDartFunc 进行调用,进一步就是对 _sendMessage 进行调用。

Dart 回调注册

在 JavascriptCoreRuntime 的构造方法中,通过一系列 JavaScriptCore 的底层 API 将 sendMessageBridgeFunction 注册到 JavaScript 引擎,成为 JavaScript 侧的 sendMessage 原生方法。

具体涉及的 JavaScriptCore API 有:

  • jSObjectMakeFunctionWithCallback:向 JavaScript 引擎中注册一个原生方法,这里经过 Dart FFI 包装之后就是注入一个 Dart 方法
  • JSObjectSetProperty:向 JavaScript 实例动态设置一个属性,如果该实例是 Global 对象的话,就是设置一个全局方法/属性

多引擎冲突问题

sendMessage 消息通信机制存在一个问题:当它在单引擎下工作的很好,如果存在多个 JavascriptCoreRuntime 实例后,开始出现问题:

前面说道,注册的回调函数实际上是 _sendMessage,这是一个成员方法。

假设有 3 个 JavaScript 引擎 EngineA、EngineB、EngineC,它们按照先后顺序创建,分别执行了不同的 JavaScript 代码。

这 3 个引擎是同时运行的,但 sendMessage 消息通信管道只有一个,而且被 EngineC 占用了。

这时候,如果 EngineA 的 JavaScript 代码调用了一个 sendMessage 方法,实际上是 EngineC 的 _sendMessage 方法被触发了。

这回造成什么问题呢?调用分发串引擎了。EngineA 中的 sendMessage 应当由 EngineA 注册的 Channel 来处理。EngineC 中不论是否存在该 Channel,都不应当响应别的 Engine 事件。

串引擎是第一个问题,第二个问题是会造成内存泄漏:

  1. 假设按照 EngineA、EngineB 顺序启动
  2. 退出 EngineB
  3. 这时查看内存会看到仍然存在两个 JavascriptCoreRuntime 实例
  4. 原因是 EngineB 的 _sendMessage 还挂在引擎里
  5. 回到 EngineA 后,JavaScript 代码调用的 sendMessage 实际上是由(应该被释放却没释放)的 EngineB 来处理。

释放 dispose

JavascriptCoreRuntime 中提供了一个 dispose 方法,内部调用了:

jSContextGroupRelease(_contextGroup);

我在 macOS 平台下进行测试,对 JavascriptCoreRuntime 调用 dispose 方法,该引擎占用的内存并没有被成功释放。

该 Repo 中有一个尚未 Close 的 PR:release global context group on dispose for jscore and close runtime for quickjs by wfeng1 · Pull Request #104 · abner/flutter_js (github.com)

在该 PR 中,提到 JavaScriptCore 和 QuickJS 在释放时都存在一些问题。其中,针对 JavascriptCoreRuntime 的 dispose,该 PR 补充了一个 API 调用:

jSGlobalContextRelease(_globalContext);
jSContextGroupRelease(_contextGroup);

使用该代码进行测试:相较于之前,引擎 dispose 后内存有所下降,但是仍然没有下降到没启动 JavaScript 之前的程度,也就是说仍然没有释放干净。 对比 JavascriptCoreRuntime 的启动逻辑;

_contextGroup = jSContextGroupCreate();
_globalContext = jSGlobalContextCreateInGroup(_contextGroup, nullptr);
_globalObject = jSContextGetGlobalObject(_globalContext);

从中可以看到,创建和销毁时对等的,从生命周期角度时完备的。

那为什么仍然没放干净呢?有两种可能:

  1. JavaScriptCore 确实没有释放干净
  2. JavaScriptCore 释放干净了,但是我自己代码其他地方有泄漏